########################################################################
#  Searx-qt - Lightweight desktop application for SearX.
#  Copyright (C) 2020  CYBERDEViL
#
#  This file is part of Searx-qt.
#
#  Searx-qt is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  Searx-qt is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
#
########################################################################

import time

from PyQt5.QtWidgets import (
    QWidget,
    QVBoxLayout,
    QLabel,
    QTableView,
    QHeaderView,
    QAbstractItemView,
    QHBoxLayout,
    QFormLayout,
    QCheckBox,
    QMenu,
    QComboBox,
    QSizePolicy,
    QScrollArea,
    QSplitter,
    QFrame,
    QWidgetAction
)

from PyQt5.QtCore import pyqtSignal, Qt

from PyQt5.QtGui import QGuiApplication

from searxqt.widgets.groupBox import CollapsableGroupBox
from searxqt.widgets.buttons import Button

from searxqt.models.instances import InstanceTableModel


class InstancesView(QWidget):
    def __init__(self, model, instanceSelecter, parent=None):
        """
        @type model: InstanceModelFilter
        @type instanceSelecter: InstanceSelecterModel
        @type parent: QObject
        """
        QWidget.__init__(self, parent=parent)
        layout = QVBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)

        self._model = model
        self._instanceSelecter = instanceSelecter
        self._tableModel = InstanceTableModel(model, self)

        headLabel = QLabel("<h2>Instances</h2>", self)
        layout.addWidget(headLabel)

        # Splitter
        self._splitter = QSplitter(self)
        self._splitter.setOrientation(Qt.Vertical)
        layout.addWidget(self._splitter)

        # Filter scroll area
        self._scrollArea = QScrollArea(self._splitter)
        self._scrollContentWidget = QWidget(self._scrollArea)
        scrollLayout = QVBoxLayout(self._scrollContentWidget)

        self.filterWidget = FilterWidget(model, self._scrollContentWidget)
        self._scrollArea.setWidget(self._scrollContentWidget)
        self._scrollArea.setFrameShape(QFrame.NoFrame)
        self._scrollArea.setWidgetResizable(True)
        self._scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self._scrollArea.setAlignment(
            Qt.AlignLeading | Qt.AlignLeft | Qt.AlignTop
        )
        scrollLayout.addWidget(self.filterWidget, 0, Qt.AlignTop)

        # Bottom widgets
        bottomWidget = QWidget(self._splitter)
        bottomLayout = QVBoxLayout(bottomWidget)

        self.statsWidget = InstancesStatsWidget(self._model, bottomWidget)
        bottomLayout.addWidget(self.statsWidget)

        updateButton = Button("Update", bottomWidget)
        bottomLayout.addWidget(updateButton)

        self._tableView = InstanceTableView(model, self._tableModel, self)
        bottomLayout.addWidget(self._tableView)

        updateButton.clicked.connect(self.__updateClicked)
        self.filterWidget.changed.connect(self.__filterChanged)
        self._tableView.selectionModel().selectionChanged.connect(
                                                self.__selectionChanged)
        self._instanceSelecter.instanceChanged.connect(self.__instanceChanged)

    def __instanceChanged(self, url):
        """ Select and scroll to table item with passed url.

        @param url: Instance url to select and scroll to.
        @type url: str
        """
        if url:
            rowIndex = self._tableModel.getByUrl(url)
            modelIndex = self._tableModel.index(rowIndex, 0)
            self._tableView.setCurrentIndex(modelIndex)
            self._tableView.scrollTo(
                modelIndex,
                QAbstractItemView.PositionAtCenter
            )

    def __selectionChanged(self):
        """ Let the InstanceSelecter know that the current instance has
        changed. (Selected by the user.)
        """
        self._instanceSelecter.instanceChanged.disconnect(
                                                self.__instanceChanged)

        selectionModel = self._tableView.selectionModel()
        if selectionModel.hasSelection():
            rows = selectionModel.selectedRows()
            selectedIndex = rows[0].row()

            self._instanceSelecter.currentUrl = (
                self._tableModel.getByIndex(selectedIndex)
            )
        else:
            self._instanceSelecter.currentUrl = ""

        self._instanceSelecter.instanceChanged.connect(self.__instanceChanged)

    def __updateClicked(self, checked):
        self._model.update()

    def __filterChanged(self):
        self._model.updateKwargs(self.filterWidget.data())

    def saveSettings(self):
        header = self._tableView.horizontalHeader()

        data = {
            'hiddenColumnIndexes': self._tableView.getHiddenColumnIndexes(),
            'headerState': header.saveState()
        }

        return data

    def loadSettings(self, data):
        self._tableView.setHiddenColumnIndexes(
                data.get('hiddenColumnIndexes', [])
        )

        if 'headerState' in data:
            header = self._tableView.horizontalHeader()
            header.restoreState(data['headerState'])


class InstanceTableView(QTableView):
    def __init__(self, filterModel, model=None, parent=None):
        QTableView.__init__(self, parent=parent)

        self._filterModel = filterModel

        if model:
            self.setModel(model)

        self.setAlternatingRowColors(True)
        self.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.setEditTriggers(QAbstractItemView.NoEditTriggers)
        self.setSortingEnabled(True)
        self.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel)

        # Horizontal header
        header = self.horizontalHeader()
        header.setSectionResizeMode(QHeaderView.ResizeToContents)
        header.setSectionsMovable(True)

    def _hideColumn(self, index, state):
        state = state == 0
        self.setColumnHidden(index, state)

    def getHiddenColumnIndexes(self):
        columnIndex = 0
        indexes = []
        for columnData in self.model().getColumns():

            # Set current value
            if self.isColumnHidden(columnIndex):
                indexes.append(columnIndex)

            columnIndex += 1
        return indexes

    def setHiddenColumnIndexes(self, indexes):
        for index in indexes:
            self._hideColumn(index, True)

    """ Re-implementations
    """
    def contextMenuEvent(self, event):
        selectionModel = self.selectionModel()

        if selectionModel.hasSelection():
            menu = QMenu(self)

            # Add to blacklist
            addToBlacklistAction = menu.addAction("Add to blacklist")

            # Copy URL
            copyUrlAction = menu.addAction("Copy URL")
            menu.addSeparator()

            # Select all
            selectAllAction = menu.addAction("Select All")
            menu.addSeparator()

            # Column options
            columnsMenu = QMenu("Columns", self)
            menu.addMenu(columnsMenu)

            columnIndex = 0
            for columnData in self.model().getColumns():
                action = QWidgetAction(columnsMenu)
                widget = QCheckBox(columnData.name, columnsMenu)
                widget.setTristate(False)
                action.setDefaultWidget(widget)

                # Set current value
                if not self.isColumnHidden(columnIndex):
                    widget.setChecked(True)

                columnsMenu.addAction(action)

                widget.stateChanged.connect(
                    lambda state, index=columnIndex:
                        self._hideColumn(index, state)
                )

                columnIndex += 1

            action = menu.exec_(self.mapToGlobal(event.pos()))
            if action == addToBlacklistAction:

                indexes = selectionModel.selectedRows()
                filter_ = self._filterModel.filter()
                toAdd = []
                for index in indexes:
                    url = self.model().getByIndex(index.row())
                    toAdd.append(url)

                self._filterModel.updateKwargs(
                    {
                        'skipUrl': filter_['skipUrl'] + toAdd
                    }
                )

            elif action == copyUrlAction:
                index = selectionModel.selectedRows()[0]
                url = self.model().getByIndex(index.row())
                clipboard = QGuiApplication.clipboard()
                clipboard.setText(url)

            elif action == selectAllAction:
                self.selectAll()


class UrlBlacklistItem(QWidget):
    deleted = pyqtSignal(str)  # version string

    def __init__(self, url, parent=None):
        QWidget.__init__(self, parent=parent)
        layout = QHBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)

        self._urlStr = url
        label = QLabel(url, self)
        delButton = Button("X", self)
        delButton.setSizePolicy(
            QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
        )

        layout.addWidget(label)
        layout.addWidget(delButton)

        delButton.clicked.connect(self.__delMe)

    def str(self): return self._urlStr

    def __delMe(self):
        self.deleted.emit(self.str())
        self.deleteLater()


class UrlFilterWidget(QWidget):
    def __init__(self, model, parent=None):
        QWidget.__init__(self, parent=parent)
        self._model = model
        self._items = []

        QVBoxLayout(self)
        self.layout().setContentsMargins(0, 0, 0, 0)
        self.layout().setSpacing(0)

        self._model.changed.connect(self.__modelChanged)

    def __modelChanged(self):
        filter = self._model.filter()
        for url in filter['skipUrl']:
            self.__add(url, updateModel=False)

    def __add(self, url, updateModel=True):
        if url not in self._items:
            item = UrlBlacklistItem(url, self)
            self._items.append(url)
            item.deleted.connect(self.delete)
            self.layout().addWidget(item)

    def delete(self, url):
        self._items.remove(url)
        self._model.updateKwargs(self.data())

    def data(self):
        return {'skipUrl': self._items.copy()}


class NetworkFilterWidget(QWidget):
    def __init__(self, model, parent=None):
        """
        @type model: InstancesModel
        """
        QWidget.__init__(self, parent=parent)
        self._model = model
        layout = QHBoxLayout(self)

        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)

        self._webBox = QCheckBox("Web", self)
        self._torBox = QCheckBox("Tor", self)
        self._i2pBox = QCheckBox("i2p", self)

        self._webBox.setSizePolicy(
            QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
        )
        self._torBox.setSizePolicy(
            QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
        )
        self._i2pBox.setSizePolicy(
            QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
        )

        layout.addWidget(self._webBox, 0, Qt.AlignLeft)
        layout.addWidget(self._torBox, 0, Qt.AlignLeft)
        layout.addWidget(self._i2pBox, 1, Qt.AlignLeft)

        self._model.changed.connect(self._modelChanged)

        self._webBox.stateChanged.connect(self.__changed)
        self._torBox.stateChanged.connect(self.__changed)
        self._i2pBox.stateChanged.connect(self.__changed)

    def __changed(self, state):
        self._model.changed.disconnect(self._modelChanged)
        self._model.updateKwargs(self.data())
        self._model.changed.connect(self._modelChanged)

    def _modelChanged(self):
        self._webBox.stateChanged.disconnect(self.__changed)
        self._torBox.stateChanged.disconnect(self.__changed)
        self._i2pBox.stateChanged.disconnect(self.__changed)

        self._webBox.setChecked(False)
        self._torBox.setChecked(False)
        self._i2pBox.setChecked(False)

        filter = self._model.filter()

        # Nothing is checked
        if not filter['ext'] and not filter['skipExt']:
            self._webBox.setChecked(True)
            self._torBox.setChecked(True)
            self._i2pBox.setChecked(True)

        # Only web checked
        elif 'onion' in filter['skipExt'] and 'i2p' in filter['skipExt']:
            self._webBox.setChecked(True)

        # Web + Tor
        elif 'i2p' in filter['skipExt'] and not filter['ext']:
            self._webBox.setChecked(True)
            self._torBox.setChecked(True)

        # Web + i2p
        elif 'onion' in filter['skipExt'] and not filter['ext']:
            self._webBox.setChecked(True)
            self._torBox.setChecked(True)

        # Tor + i2p
        elif 'onion' in filter['ext'] and 'i2p' in filter['ext']:
            self._i2pBox.setChecked(True)
            self._torBox.setChecked(True)

        # Tor
        elif 'onion' in filter['ext']:
            self._torBox.setChecked(True)

        # i2p
        elif 'i2p' in filter['ext']:
            self._i2pBox.setChecked(True)

        self._webBox.stateChanged.connect(self.__changed)
        self._torBox.stateChanged.connect(self.__changed)
        self._i2pBox.stateChanged.connect(self.__changed)

    def data(self):
        result = {
            'ext': [],  # required
            'skipExt': []  # skip
        }

        """
        [ ] web		[ ] tor		[ ] i2p
        [x] web		[x] tor		[x] i2p
        result = {
            'ext': [],
            'skipExt': []
        }

        [x] web
        [ ] tor
        [ ] i2p
        result = {
            'ext': [],
            'skipExt': ['onion', 'i2p']
        }

        [x] web
        [x] tor
        [ ] i2p
        result = {
            'ext': [],
            'skipExt': ['i2p']
        }

        [x] web
        [ ] tor
        [x] i2p
        result = {
            'ext': [],
            'skipExt': ['onion']
        }

        [ ] web
        [x] tor
        [x] i2p
        result = {
            'ext': ['onion', 'i2p'],
            'skipExt': []
        }

        [ ] web
        [ ] tor
        [x] i2p
        result = {
            'ext': ['i2p'],
            'skipExt': []
        }

        [ ] web
        [x] tor
        [ ] i2p
        result = {
            'ext': ['onion'],
            'skipExt': []
        }
        """

        # Nothing or all checked
        if ((not self._webBox.isChecked()
                and not self._torBox.isChecked()
                and not self._i2pBox.isChecked())
                or (self._webBox.isChecked()
                    and self._torBox.isChecked()
                    and self._i2pBox.isChecked())):

            return result

        # Only web checked
        if (self._webBox.isChecked()
                and not self._torBox.isChecked()
                and not self._i2pBox.isChecked()):

            result['skipExt'].append('onion')
            result['skipExt'].append('i2p')
            return result

        # Web + Tor
        if (self._webBox.isChecked()
                and self._torBox.isChecked()
                and not self._i2pBox.isChecked()):
            result['skipExt'].append('i2p')
            return result

        # Web + i2p
        if (self._webBox.isChecked()
                and not self._torBox.isChecked()
                and self._i2pBox.isChecked()):

            result['skipExt'].append('onion')
            return result

        # Tor + i2p
        if (not self._webBox.isChecked()
                and self._torBox.isChecked()
                and self._i2pBox.isChecked()):

            result['ext'].append('onion')
            result['ext'].append('i2p')
            return result

        # Tor
        if (not self._webBox.isChecked()
                and self._torBox.isChecked()
                and not self._i2pBox.isChecked()):

            result['ext'].append('onion')
            return result

        # i2p
        if (not self._webBox.isChecked()
                and not self._torBox.isChecked()
                and self._i2pBox.isChecked()):

            result['ext'].append('i2p')
            return result

        return result


class VersionItem(QWidget):
    deleted = pyqtSignal(str)  # version string

    def __init__(self, versionStr, parent=None):
        QWidget.__init__(self, parent=parent)
        layout = QHBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)

        self._versionStr = versionStr
        label = QLabel(versionStr, self)
        delButton = Button("X", self)
        delButton.setSizePolicy(QSizePolicy(
            QSizePolicy.Maximum, QSizePolicy.Fixed))

        layout.addWidget(label)
        layout.addWidget(delButton)

        delButton.clicked.connect(self.__delMe)

    def str(self): return self._versionStr

    def __delMe(self):
        self.deleted.emit(self.str())
        self.deleteLater()


class VersionFilter(QWidget):
    def __init__(self, model, parent=None):
        """
        @type model: InstanceModelFilter
        """
        QWidget.__init__(self, parent=parent)
        self._model = model
        self._items = {}

        layout = QVBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)

        # Add new
        horizontalLayout = QHBoxLayout()
        layout.addLayout(horizontalLayout)

        self.comboBox = QComboBox(self)
        self.addButton = Button("Add", self)
        self.addButton.setSizePolicy(
            QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
        )

        horizontalLayout.addWidget(self.comboBox)
        horizontalLayout.addWidget(self.addButton)

        self.addButton.clicked.connect(self.__addClicked)
        self._model.changed.connect(self.__modelChanged)

    def __addClicked(self):
        """ Add button clicked -- will add the current selected version
        of the combobox to the version filter list.

        This wil trigger the following:
        1) self.__add
            Adds the version string as a key to self._items
        2) self._model.updateKwargs
            Update the model
        3) self.__modelChanged
            Model has been changed so self.__modelChanged is called. This
            will re-generate the view.
        """
        self.__add(self.comboBox.currentText())
        self._model.updateKwargs(self.data())  # update the model.

    def __fromItemDelete(self, versionStr):
        """ Called when a VersionItem emits deleted (It's 'X' aka delete
        button has been pressed).

        This will trigger the following:
        1) self.delete
            Deletes the version (key) from self._items and removes from
            the widget from the view.
        2) self._model.updateKwargs
            Update the model.
        3) Model has been changed so self.__modelChanged is called. This
            will re-generate the view.
        """
        self.delete(versionStr)
        self._model.updateKwargs(self.data())

    def _compileVersions(self):
        """ Fill the combobox with available version strings
        """
        allVersions = []
        self.comboBox.clear()
        for url, instance in self._model.parentModel().items():
            if instance.version and instance.version not in allVersions:
                allVersions.append(instance.version)

                if instance.version not in self._items.keys():
                    self.comboBox.addItem(instance.version)

    def __modelChanged(self):
        self.clear()  # clear the versions list

        filter = self._model.filter()
        for versionStr in filter['version']:
            # Add the version to the list
            self.add(versionStr)

        self._compileVersions()  # re-generate the combobox items

    def data(self):
        result = {
            'version': [],  # required
            'skipVersion': []  # skip (TODO unused)
        }

        for versionStr in self._items.keys():
            result['version'].append(versionStr)

        return result

    def clear(self):
        """ Clears added version items (on the view side only)
        """
        keys = list(self._items.keys())
        for versionStr in keys:
            self.delete(versionStr)

    def __add(self, versionStr):
        self._items.update({versionStr: None})

    def add(self, versionStr):
        item = VersionItem(versionStr, self)
        self._items.update({versionStr: item})
        item.deleted.connect(self.__fromItemDelete)
        self.layout().addWidget(item)

    def delete(self, versionStr):
        if self._items[versionStr]:
            # delete the widget if exists.
            self._items[versionStr].deleteLater()
        del self._items[versionStr]


class CheckboxFilter(QCheckBox):
    def __init__(self, model, key, parent=None):
        """
        @type model: InstancesModel

        @param key: key to use (from model.filter())
        @type key: str
        """
        QCheckBox.__init__(self, parent=parent)
        self._model = model
        self._key = key

        self._model.changed.connect(self.__modelChanged)
        self.stateChanged.connect(self.__stateChanged)

    def __modelChanged(self):
        """ Model changed, update the checkbox.
        """
        filter_ = self._model.filter()
        state = filter_.get(self._key, True)
        self.setChecked(state)

    def __stateChanged(self):
        """ Checkbox state changed, update the model.
        """
        state = self.isChecked()
        self._model.updateKwargs({self._key: state})


class FilterWidget(CollapsableGroupBox):
    changed = pyqtSignal()

    def __init__(self, model, parent=None):
        CollapsableGroupBox.__init__(self, txt="Filter", parent=parent)

        layout = QFormLayout(self.contentWidget)
        layout.setLabelAlignment(
            Qt.AlignLeading | Qt.AlignLeft | Qt.AlignTop
        )

        self._networkFilter = NetworkFilterWidget(model, self)
        layout.addRow(QLabel("Network:"), self._networkFilter)

        self._asnPrivacyFilter = CheckboxFilter(model, 'asnPrivacy', self)
        label = QLabel("Require ASN privacy:")
        label.setWordWrap(True)
        label.setToolTip(
            "Filter out instances that run their server at a known\n"
            "malicious network like Google, CloudFlare, Akamai etc..\n"
            "\n"
            "This does not give any guarantee, it only filters known\n"
            "privacy violators!"
        )
        layout.addRow(label, self._asnPrivacyFilter)

        self._ipv6Filter = CheckboxFilter(model, 'ipv6', self)
        label = QLabel("Require IPv6:")
        label.setWordWrap(True)
        label.setToolTip(
            "Only instances with at least one ipv6-address remain if checked"
        )
        layout.addRow(label, self._ipv6Filter)

        self._versionFilter = VersionFilter(model, self)
        layout.addRow(QLabel("Version:"), self._versionFilter)

        self._urlFilter = UrlFilterWidget(model, self)
        layout.addRow(QLabel("Blacklist:"), self._urlFilter)


class InstancesStatsWidget(CollapsableGroupBox):
    def __init__(self, model, parent=None):
        CollapsableGroupBox.__init__(self, txt="Stats", parent=parent)
        self._model = model

        layout = QFormLayout(self.contentWidget)

        self._countLabel = QLabel("0", self)
        layout.addRow(QLabel("Total count:"), self._countLabel)

        self._filterCountLabel = QLabel("0", self)
        layout.addRow(
            QLabel("After filter count:"),
            self._filterCountLabel
        )

        self._lastUpdateLabel = QLabel("0", self)
        self._lastUpdateLabel.setWordWrap(True)
        layout.addRow(QLabel("Last update:"), self._lastUpdateLabel)

        self._model.changed.connect(self.__modelChanged)

    def __modelChanged(self):
        self._countLabel.setText(str(len(self._model.parentModel())))
        self._filterCountLabel.setText(str(len(self._model)))
        self._lastUpdateLabel.setText(
            time.asctime(
                time.localtime(
                    self._model.parentModel().lastUpdated()
                )
            )
        )

    def setCount(self, count):
        self._countLabel.setText(str(count))
